|     java thinking in java inheritance composition   |    

组合

组合就是把别的类的实例,放到一个新的类里去。

练习 1

Create a simple class. Inside a second class, define a reference to an object of the first class. Use lazy initialization to instantiate this object.

package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;

/**
 *  Compose of simple objects
 */
public class IniObj {

    public String toString(){
        return this.obj1.s;
    }

    public SimpObj obj1;

    public static void main(String[] args){
        IniObj testIniObj = new IniObj();
        //惰性初始化
        testIniObj.obj1=new SimpObj();	//需要先创建SimpObj实例
        testIniObj.obj1.s="Now I need to use it!";	才能对其中的值s赋值
        System.out.println(testIniObj.toString());
    }
}

class SimpObj {
    //not initialized
    String s;
}

继承

为了继承,一般规则是将所有数据成员设置成private,而将所有成员方法都设置成public,或protected。

这个原则还是延续了一贯的“数据不可见,仅接口方法可见”的原则。这个时候,继承的子类对类中的数据是无法直接操作的,唯一可用的就是继承下来的饿接口方法。

练习 2

Inherit a new class from class Detergent. Override scrub( ) and add a new method called sterilize( ).

/**
 *  Tide is a famous brand of laundry detergent
 *  inherit from Detergent class in the book
 */
package com.ciaoshen.thinkinjava.chapter7;

import java.util.*;


public class Tide extends Detergent {

    public void sterilize(){append(" sterilize() ");}
    public void scrub(){append(" Tide."); super.scrub();}

    /**
     *  main method
     *  @param args void
     */
    public static void main(String[] args){
        Tide t = new Tide();
        t.dilute();
        t.apply();
        t.scrub();
        t.foam();
        t.sterilize();
        System.out.println(t);
    }
}

//Output:	Cleanser dilute() apply() Tide.Detergent.scrub() scrub() foam() sterilize()

继承子类的构造

!!注意:前面说基类数据一般都设成private。但这样的话,继承后子类是无法访问自己的数据的。

比如在练习2中,Tide类是继承自Detergent类。Detergent中有私有字段,private String s;。但有一点非常奇怪:当我们创建Tide型新对象t的时候,t.s还是受private保护,无法访问的。

原因就涉及到Java对继承特性的实现方法:当创建一个子类对象的时候,子类对象里就包含着一个父类对象。也就是说:Java自动在子类构造器中,插入父类构造器。

//爷爷
class Art {
  Art() { print("Art constructor"); }
}
//爸爸
class Drawing extends Art {
  Drawing() { print("Drawing constructor"); }
}
//儿子
public class Cartoon extends Drawing {
  public Cartoon() { print("Cartoon constructor"); }

  public static void main(String[] args) {
    Cartoon x = new Cartoon();
  }
}

//Output:
//	Art constructor		//要造爸爸,还得先造爷爷
//	Drawing constructor	//造儿子前,先要造个爸爸
//	Cartoon constructor	//造儿子

//所以实际上儿子构造器干了这些事儿:
public Cartoon() {
	Drawing() {
		Art() {
			print("Art constructor");
		}
		print("Drawing constructor");
	}
	print("Cartoon constructor");
}

想继承父类带参数的构造器?

必须显式地写出来,而且必须是在构造器的起始处,不然默认调用无参数的构造器。

//爷爷
class Art {
  Art() { print("Art constructor"); }
  Art(String name) { print(name+" Art constructor"); }
}
//爸爸
class Drawing extends Art {
  Drawing() { print("Drawing constructor"); }
  Drawing(String name) { super("爷爷:"); print(name+" Drawing constructor"); }
}
//儿子
public class Cartoon extends Drawing {
  public Cartoon() { print("Cartoon constructor"); }
  public Cartoon(String name) { super("爸爸:"); print(name+" Cartoon constructor"); }

  public static void main(String[] args) {
    Cartoon x = new Cartoon("儿子");
  }
}

//输出:
//Output:
//	爷爷: Art constructor		
//	爸爸: Drawing constructor
//	儿子: Cartoon constructor

练习 3,4,5

Exercise 3: (2) Prove the previous sentence.

Exercise 4: (2) Prove that the base-class constructors are (a) always called and (b) called before derived-class constructors.

Exercise 5: (1) Create two classes, A and B, with default constructors (empty argument lists) that announce themselves. Inherit a new class called C from A, and create a member of class B inside C. Do not create a constructor for C. Create an object of class C and observe the results.

package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;

//MacBook is a kind of Laptop
public class MacBook extends Laptop {
    //constructor
    MacBook(){
        System.out.println("of Apple Inc.");
    }

    private RetinaScreen macScreen=new RetinaScreen();

    //main method
    public static void main(String[] args){
        MacBook macDeWei=new MacBook();
    }
}

//base class
class Laptop{
    //constructor
    Laptop(){
        System.out.print("I am a Laptop ");
    }
}

class RetinaScreen{
    RetinaScreen(){
        System.out.println("This is a Retina screen. ");
    }
}

//	练习3,4 output:
//	I am a Laptop of Apple Inc.
//	练习5: 用标注去掉MacBook的构造函数以后:
//	Output: I am a Laptop This is a Retina screen.

练习 6

Prove that if you don’t call the base-class constructor in BoardGame( ), the compiler will complain that it can’t find a constructor of the form Game( ).

/**
 *  证明子类会自动调动父类构造器
 *  @author wei.shen
 *  @version 1.0
 */
package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;

//public class
public class Chess extends BoardGame {
    Chess() {
        super(11);
        System.out.println("Chess constructor");
    }
    public static void main(String[] args) {
        Chess x = new Chess();
    }
}

//only visible in package
class BoardGame extends Game {
    BoardGame(int i) {
        //super(i);		//如果父类构造器带参数,必须显式调用
        System.out.println("BoardGame constructor");
    }
}

//only visible in package
class Game {
    Game(int i) {
        System.out.println("Game constructor");
    }
}

中间一行super(i)被注释掉了,BoardGame无法调用Game的构造器。如果取消注释,就会恢复正常。

##系统报错
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/Chess.java:25: error: constructor Game in class Game cannot be applied to given types;
    BoardGame(int i) {
                     ^
  required: int
  found: no arguments
  reason: actual and formal argument lists differ in length
1 error

练习 7,8

package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;

//MacBook is a kind of Laptop
public class MacBook extends Laptop {

    //default constructor: exercise 8
    MacBook(){
        super(0);
        this.owner="???";
        System.out.println(" of Apple Inc. ");
        System.out.println(this.owner+" bought me. ");
        this.macScreen=new RetinaScreen(0);
    }

    //constructor: exercise 7
    MacBook(int macId, int screenPixels, String theOwner){
        super(macId);
        this.owner=theOwner;
        System.out.println(" of Apple Inc. ");
        System.out.println(theOwner+" bought me. ");
        this.macScreen=new RetinaScreen(screenPixels);
    }

    private String owner;
    private RetinaScreen macScreen;

    /**
     *  main method
     *  @param args void
     */
    public static void main(String[] args){
        //execise 7
        MacBook macDeQui=new MacBook(12345, 8000000, "Wei");
        //exercise 8
        MacBook macDeWei=new MacBook();

    }
}

//base class
class Laptop{
    //constructor
    Laptop(int id){
        System.out.print("I am the Laptop No."+id);
    }
}

//one of the MacBook component
class RetinaScreen{
    RetinaScreen(int pixels){
        System.out.println("I have a Retina screen with "+pixels+" pixels!");
    }
}

输出结果:

//练习7
I am the Laptop No.12345of Apple Inc.
Wei bought me.
I have a Retina screen with 8000000 pixels!
//练习8
I am the Laptop No.0of Apple Inc.
??? bought me.
I have a Retina screen with 0 pixels!

练习 9

Exercise 9: (2) Create a class called Root that contains an instance of each of the classes (that you also create) named Component1, Component2, and Component3. Derive a class Stem from Root that also contains an instance of each “component.” All classes should have default constructors that print a message about that class.

Exercise 10: (1) Modify the previous exercise so that each class only has non-default constructors.

/**
 *  Stem(树干)代表一棵树,继承自父类Root(根茎)
 *  Stem(树干)开始有自己的“name(名字)”
 *  父类Root(根茎)有三个组成部分:树皮,木质部,和木髓
 *  @author wei.shen@iro.umontreal.ca
 *  @version 1.0
 *  这个类写完,编译一次过,拥吻自己,撒花
 */

package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;

//树干
//the only visible class
public class Stem extends Root{

    //default constructor
    Stem(){}
    //constructor with params
    Stem(String phloemColor,int xylemLayer,int pithDiameter,String treeName){
        super(phloemColor,xylemLayer,pithDiameter);
        this.name=treeName;
        System.out.println("It is called "+this.name+".");
    }

    //private fields
    private String name="Tree"; //default name

    /**
     *  MAIN
     *  @param args void
     */
    public static void main(String[] args){
        //Exercise 9
        Stem newTree=new Stem();
        //Exercise 10:雪曼将军树是目前世界上最大的树
        Stem generalSherman=new Stem("red",2500,500,"General Sherman");
    }
}

//根茎
//base class of Stem
class Root {

    //default constructor
    Root(){
        this.treePhloem=new Phloem();
        this.treeXylem=new Xylem();
        this.treePith=new Pith();
    }

    //constructor with params
    Root(String phloemColor,int xylemLayer,int pithDiameter){
        this.treePhloem=new Phloem(phloemColor);
        this.treeXylem=new Xylem(xylemLayer);
        this.treePith=new Pith(pithDiameter);
    }

    //private fields
    private Phloem treePhloem;
    private Xylem treeXylem;
    private Pith treePith;
}

//树皮(韧皮部)
//one of the three components of Root
class Phloem {
    //friendly default constructor
    Phloem(){System.out.println("The outer of a tree is the Phloem.");}
    //constructor with param
    Phloem(String color){System.out.println("This tree has "+color+" Phloem.");}
}

//木质部(木导管纤维)
//one of the three components of Root
class Xylem {
    //friendly default constructor
    Xylem(){System.out.println("The inner of a tree is the Xylem.");}
    //constructor with param
    Xylem(int numLayer){System.out.println("We can estimate the age of this tree by its Annual Growth Rings. This tree is "+numLayer+" years old.");}
}

//木髓(最里层)
//one of the three components of Root
class Pith {
    //friendly default constructor
    Pith(){System.out.println("The core of a tree is the Pith.");}
    //constructor with param
    Pith(int diameter){System.out.println("The diameter of its Pith is "+diameter+"CM.");}
}

输出:

##Exercise 9:
The outer of a tree is the Phloem.
The inner of a tree is the Xylem.
The core of a tree is the Pith.

##Exercise 10:
This tree has red Phloem.
We can estimate the age of this tree by its Annual Growth Rings. This tree is 2500 years old.
The diameter of its Pith is 500CM.
It is called General Sherman.

代理

代理其实更像组合,而不是继承。用组合的方法,给原先的类穿个衣服,实现了继承的功能。而且比继承强大,因为可以选择性继承。 Java不直接支持继承。在Java里,继承更像是一种设计模式,把旧类包装成一个新类。

//给飞船控制器贴个名字,就变一艘飞船了
public class SpaceShipDelegation {
  private SpaceShipControls controls =
    new SpaceShipControls();	//飞船就只有一个控制器
  private String name;	//只不过贴个名字而已  

  //所有控制器的方法都重新起个名字
  public SpaceShipDelegation(String name) {
    this.name = name;
  }
  // Delegated methods:
  public void back(int velocity) {
    controls.back(velocity);
  }
  public void down(int velocity) {
    controls.down(velocity);
  }
  public void forward(int velocity) {
    controls.forward(velocity);
  }
  public void left(int velocity) {
    controls.left(velocity);
  }
  public void right(int velocity) {
    controls.right(velocity);
  }
  public void turboBoost() {
    controls.turboBoost();
  }
  public void up(int velocity) {
    controls.up(velocity);
  }

练习 11

Exercise 11: (3) Modify Detergent.java so that it uses delegation.

/**
 *  Encapsulate the Detergent Class using Delegation
 */
package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;

public class DetergentDelegation {

    //全新接口
    //Encapsulate the public methods of Cleanser
    public void append(String a) { theCleanser.append(a); }
    public void dilute() { theCleanser.dilute(); }
    public void apply() { theCleanser.apply(); }
    public String toString() { return theCleanser.toString(); }
    public void scrub() {
        append(" Detergent.");
        theCleanser.scrub(); // Call base-class version
    }
    // Add a new method to the interface:
    public void foam() { theCleanser.append(" foam()"); }

    //private fields: 对用户完全隐藏Cleanser
    //compose of Cleanser class
    private Cleanser theCleanser=new Cleanser();

    /**
     *  main method
     *  @param args void
     */
    public static void main(String[] args) {
        DetergentDelegation x = new DetergentDelegation();

        x.dilute();
        x.apply();
        x.scrub();
        x.foam();
        System.out.println(x);
        //System.out.println(x.theCleanser);    //内部theCleanser对用户不可见
    }
}

//Output: Cleanser dilute() apply() Detergent. scrub() foam()

@Override标签

很明显,继承的子类可以重载父类方法。只要子类的方法不完全和父类中的方法重合(返回值,参数,方法名完全相同),新方法会被重载,而不是重写

真的需要重写覆盖父类方法的时候,为了确保我们不是仅仅重载了方法,可以在需要重写的方法前,加一个@Override标签。这样系统会自动检察是不是重写覆盖成功,如果没有会报错。

练习 13

Exercise 13: Create a class with a method that is overloaded three times. Inherit a new class, add a new overloading of the method, and show that all four methods are available in the derived class.

三个重载方法。!!注意:仅返回值不同,不能重载方法。编译器通不过,不知道加载哪个。

package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;

public class TestOverload {

    //public method
    public void overloadedMethod(){
        System.out.println("overloadedMethod(): No parameter");
    }
    public void overloadedMethod(int i){
        System.out.println("overloadedMethod(): int");
    }
    public void overloadedMethod(String s){
        System.out.println("overloadedMethod(): String");
    }


    /******************************************************
     *
     *  这样以不同的返回值,区分不同方法,编译器不接受。
     *
    public int overloadedMethod(int i){
        System.out.println("overloadedMethod(): int; return int");
        return i;
    }
     *
     ******************************************************/
}

继承类继续重载:

package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;

public class TestOverride extends TestOverload{

    //another overload
    public void overloadedMethod(char c){
        System.out.println("overloadedMethod(): char");
    }

    //now I want to override
    //取消下面@Override的注释,会提示重写失败。
    //@Override
    public void overloadedMethod(String s, int a){
        System.out.println("overloadedMethod(): String is overrided");
    }

    /**
     *  MAIN
     *  @param args void
     */
    public static void main(String[] args){
        TestOverride myOverride=new TestOverride();

        myOverride.overloadedMethod();
        myOverride.overloadedMethod(1);
        myOverride.overloadedMethod("Hello");
        myOverride.overloadedMethod((char)1);
    }
}

结果四个方法都重载成功:

overloadedMethod(): No parameter
overloadedMethod(): int
overloadedMethod(): String
overloadedMethod(): char

但如果取消@Override的注释,@Override会检查最后那个方法是否重写成功。如果重写失败,抛出错误:

/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/TestOverride.java:20: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error

怎么用组合和继承?慎用继承!

到底是用组合好,还是继承好。无法简单地下结论。作者给出两者形象化的思考:

  1. “继承”IS-A关系。就是使用某个现有通用类,有通用的一组接口,只是开发出一个特定版本。比如通用类是车->公共汽车。
  2. “组合”HAS-A关系。本质不是同一个东西,接口基本不一样,我们只是想用某些功能。比如发动机->车。

!!!注意继承要慎用!作者提醒我们:“继承”在实战中并不太常用。不应该尽可能地使用继承,而是要慎重。用之前需要问自己一个问题:是否需要从导出类向基类进行向上转型?只有必须向上转型的情况才需要用继承。

所以正确的习惯是:优先考虑“组合”“代理”。只在确实必要时才考虑“继承”

成员字段全设成private

无论是对于组合还是继承,作者反复强调需要遵守的谨慎的设计风格:

  1. 基本要求:数据字段要设成private
  2. 进一步的要求:用到组合的时候,各个部件也设成private。把部件的接口隐藏起来。为整体设计新接口。

这里提到的“接口”的概念,我觉得很好。类的本质是对一组数据和流程的封装。外部可见的应该只有我们定义的想让人看到的接口。所以理想的情况就是断掉所有数据字段的访问权。只给几个公开的访问接口。

private的好处就是,无论是对于组合还是继承,private部分的代码都是完全不可见的。就算是被继承,在子类里也没有这些字段或方法。

这时候,应该讨论一下protected的意义。在尽可能多的private的前提下,哪些方法是我希望外部子类能够继承的接口,可以法外开恩设成protected。

private构造函数

构造函数随意设成public不是一个好习惯。比较谨慎的方法可以设成private,然后写一个public接口方法,方便控制创建实例。

!!!注意需要被继承的基类,构造函数不要设成private,否则子类无法访问基类构造函数。!一般可以设成默认frendly或者protected。

练习 15

chapter5包里的InPackageTank类,私有字段,protected接口方法。

package com.ciaoshen.thinkinjava.chapter5;
import java.util.*;

public class InPackageTank {

    //default constructor
    public InPackageTank(String name){
        this.name=name;
    }

    //shot one bullet
    protected void shot(){
        if (this.bullet>0){
            this.bullet --;
        } else {
            System.out.println("Out of bullet! Please recharge!");
        }
    }   
    //recharge the bullet
    protected void charge(int inNum){
        this.bullet+=inNum;
        System.out.println("Charge successfully!");
    }   
    //reture the number of reste bullet
    protected int howManyBullet(){return this.bullet;}

    //private field
    private int bullet = 100;
    private String name = new String();
}

在chapter7包里直接调用我们的坦克:

package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;
import com.ciaoshen.thinkinjava.chapter5.*;

public class CallTank {

    /**
     *  MAIN
     *  @param args void
     */
    public static void main(String args[]){
        InPackageTank myTank = new InPackageTank("Tank1");
        System.out.println(myTank.howManyBullet());
        tank1.shot();
        tank1.shot();
        System.out.println(myTank.howManyBullet());
    }
}

结果调用不动,报错:方法是受保护的。

/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/CallTank.java:21: error: howManyBullet() has protected access in InPackageTank
        System.out.println(myTank.howManyBullet());
                                 ^
/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/CallTank.java:22: error: cannot find symbol
        tank1.shot();
        ^

继承了包外的坦克类之后,调用成功。

package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;
import com.ciaoshen.thinkinjava.chapter5.*;

public class CallTank extends InPackageTank{

    //inherit constructor
    public CallTank(String name){super(name);}

    /**
     *  MAIN
     *  @param args void
     */
    public static void main(String args[]){
        //没继承之前
        //InPackageTank myTank = new InPackageTank("Tank1");
        //继承以后可以调用Protected方法
        CallTank myTank=new CallTank("Panzerkampfwagen VI Tiger");    //二战德军最强坦克
        System.out.println(myTank.getName()+" has "+myTank.howManyBullet()+" bullets left.");
        myTank.shot();
        myTank.shot();
        System.out.println(myTank.getName()+" has "+myTank.howManyBullet()+" bullets left.");       
    }
}

最后输出:

Panzerkampfwagen VI Tiger has 100 bullets left.
Panzerkampfwagen VI Tiger has 98 bullets left.

练习 16,17

Exercise 16: (2) Create a class called Amphibian. From this, inherit a class called Frog. Put appropriate methods in the base class. In main( ), create a Frog and upcast it to Amphibian and demonstrate that all the methods still work.

Exercise 17: (1) Modify Exercise 16 so that Frog overrides the method definitions from the base class (provides new definitions using the same method signatures). Note what happens in main( ). 基类–两栖类:

package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;

public class Amphibian {

    //constructor
    public Amphibian(){this.onLand=false;}

    //public methods
    public void swim(){
        if(onLand){
            this.onLand=false;
            System.out.println("PuPu...");
        }
    }
    public void landings(){
        if(!onLand){
            this.onLand=true;
            System.out.println("DuangDuang...");
        }
    }

    //private fields
    boolean onLand;
}

导出类–青蛙:

package com.ciaoshen.thinkinjava.chapter7;
import java.util.*;

public class Frog extends Amphibian{

    //public methods
    public void swim(){
        if(onLand){
            this.onLand=false;
            System.out.println("GuaGuaGua...");
        }
    }
    public void landings(){
        if(!onLand){
            this.onLand=true;
            System.out.println("PiaPia...");
        }
    }

    //private fields

    /**
     *  MAIN
     *  @param args void
     */
    public static void main(String[] args){
        Amphibian someAmphibian=new Frog();
        someAmphibian.landings();
        someAmphibian.swim();
    }
}

青蛙类里不重写swim()landings()方法的话,照搬两栖类的结果:

DuangDuang...
PuTong...

要是重写swim()landings()方法,马上变输出青蛙的叫声:

PiaPia...
GuaGua...

这其实证明了java具有多态性。因为java属于后期绑定,编译完之后,并没有把两栖类的方法都加进去,而是根据main线程的实际调用再去确认具体行为。

Final关键字

final关键字,意味着这个字段,只能被赋值一次!

  1. 基本型加final关键字,成为常量。
  2. 对象使用final关键字很奇怪,只是引用不能再重新指向另一个对象,但对象本事还是能改的。

final字段初始化 (附带完成了练习18,19,说明final和final static的区别)

因为final关键字规定了此字段只能被赋值一次:

  1. 要么在声明的时候
  2. 要么在构造函数里
  3. 要么在block里

!!!注意:所以Final基本型字段声明的时候系统不会给默认值。因为系统给了默认值的话,我们无法再赋值了。 运行结果报错:

public class StaticFinalClass {

    //FINALFINAL 1号赋值点:声明的时候
    final public int FINALFINAL;

    //FINALFINAL 2号赋值点:构造函数
    public StaticFinalClass() {
        this.FINALFINAL=101;
        //this.FINALFINAL=102;  //一次赋值以后,不能再重新赋值
        //this.FINALFINAL=103;  // 这就是Final的意义
    }

    //FINALFINAL 3号赋值点:block
    {
        //this.FINALFINAL=104;  //一次赋值以后,不能再重新赋值
                                // 这就是Final的意义。
    }
}

!!!注意Final static全局静态常量,不能在构造函数里声明。因为,static意味着构造函数每创建一个实例都会为其赋值。但final意味着它只能被赋值一次,所以矛盾。

public class StaticFinalClass {

    //FINALSTATIC 1号赋值点:声明的时候
    final public static int FINALSTATIC;

    //FINALSTATIC 2号赋值点:static block
    static {
        FINALSTATIC=1000;
    }

    //构造函数里不能声明final static静态常量
    public StaticFinalClass() {
        //StaticFinalClass.FINALSTATIC=1001;	//报错
    }
}

公开静态常量

一般像这样带public static final关键字修饰基本型,就是典型的公开静态常量。不但编译时就确认它的值,而且全局独一份儿,而且所有包都可以访问。

public static final int CONSTANT_VALUE=100

final参数

java传递参数的时候传递的不是参数的拷贝,函数内部可以直接改变参数的值。如果给函数传递参数的饿时候加上final,函数内不能改变参数的值。这个特性主要用来向匿名内部类中传递参数用。

void with(final Gizmo g) {
    g = new Gizmo(); //g是final,不许改变g对对象的引用
}

void with(final int i) {
    i=5; //基本型就彻底不许改了
}

final方法 (顺带完成练习21)

方法被声明称final方法,继承它的子类就不能对父类中的此方法进行重写,覆盖。

//子类尝试重写父类方法
public class Son extends Father {
	public String toString(){return "I am son.";}
}

class Father {
	//父类的final方法,拒绝被重写
	final public String toString(){return "I am father.";}
}

运行后系统报错:

/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/Son.java:21: error: toString() in Son cannot override toString() in GrandFather
    public String toString(){return super.toString()+" I am the son.";}
                  ^
  overridden method is final
1 error

private final方法

其实类中的private方法,都相当于final方法,因为它对外部类来说不可见,所以没人能够重写它。

final类

类前被加上final之后,此类无法再被继承。

练习 22

练习16中的基类两栖类,被加上final限定之后,final public class Amphibian,无法被Frog继承。系统抛出错误:

/Users/Wei/java/com/ciaoshen/thinkinjava/chapter7/Frog.java:11: error: cannot inherit from final Amphibian
public class Frog extends Amphibian{
                          ^
1 error